Skip to content

fix: unwatch deleted projects to prevent zombie reindex#537

Merged
DeusData merged 4 commits into
DeusData:mainfrom
Golevka2001:main
Jul 3, 2026
Merged

fix: unwatch deleted projects to prevent zombie reindex#537
DeusData merged 4 commits into
DeusData:mainfrom
Golevka2001:main

Conversation

@Golevka2001

@Golevka2001 Golevka2001 commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Problem

When auto_index is enabled, opening a project directory triggers auto-indexing and registers the project with the file-change watcher. If the user later deletes that project via the delete_project MCP tool, the .db file is removed but the watcher entry persists.

The watcher continues polling the deleted project's git repository every 5–60 seconds. On each poll, it detects HEAD movement or a dirty working tree and invokes the full reindex pipeline — which operates on a database that no longer exists.

Reproduction scenario:

  1. Enable auto-index: codebase-memory-mcp config set auto_index true
  2. Start the MCP server from inside a git project directory
  3. The server auto-indexes and the watcher begins polling
  4. Delete the project: delete_project(project="<name>")
  5. Continue working in the same directory (git commit, pull, etc.)
  6. The watcher detects changes and attempts to reindex → error logs, wasted CPU, and unnecessary I/O on every poll cycle

cbm_watcher_unwatch() is fully implemented (watcher.c:316–332) but is never called from any production code path — only from tests.

Copilot AI review requested due to automatic review settings June 20, 2026 14:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a “zombie reindex” scenario where deleting a project removes its .db but leaves the project registered with the file-change watcher, causing repeated failed reindex attempts and wasted CPU/I/O.

Changes:

  • Add a cbm_watcher_unwatch() call to the MCP delete_project handler after deleting the project DB.
  • Guard the unwatch call with if (srv->watcher).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mcp/mcp.c
Comment on lines +1815 to +1817
if (srv->watcher) {
cbm_watcher_unwatch(srv->watcher, name);
}
@Golevka2001

Copy link
Copy Markdown
Contributor Author

Mind if I add a pending-free list? Unwatch defers the free there, poll_once drains it under lock before snapshotting—avoids the UAF without refcounting.

@Golevka2001

Copy link
Copy Markdown
Contributor Author

I have formatted the file with clang-format, please re-trigger the CI check.

@DeusData

Copy link
Copy Markdown
Owner

Thanks @Golevka2001 — unwatching deleted projects is the right fix and the deferred-free design is sound. One concern before merge: cbm_watcher_free also drains/frees pending_free, so if it can run while cbm_watcher_poll_once is mid-cycle there's a use-after-free / double-free window on the pending_free array. Please confirm the watcher thread is stopped/joined before cbm_watcher_free frees it (or guard the free with projects_lock). A small test exercising the delete→unwatch path would also help. 🙏

@DeusData

Copy link
Copy Markdown
Owner

Thanks @Golevka2001 — real bug and a clean fix: cbm_watcher_unwatch existed but had no production caller, so deleting a project left the watcher polling and re-indexing a deleted DB. Wiring it into handle_delete_project and using a deferred-free against the lock-free poll loop is the right approach (the one-cycle deferral is sound for the single watcher thread).

Two things before it can land:

  1. Regression test: please add one exercising the fix — register a watch, run the delete_project path, and assert cbm_watcher_watch_count drops to 0 (ideally also a poll-after-unwatch to guard the deferred-free). The existing watcher tests are a good template.
  2. CI hasn't run on this PR yet (no checks present — likely first-time-contributor workflow approval); it needs to be green before merge.

Nice catch on the zombie-reindex — looking forward to the test.

Signed-off-by: Gol3vka <gol3vka@163.com>
Signed-off-by: Gol3vka <gol3vka@163.com>
Signed-off-by: Gol3vka <gol3vka@163.com>
@Golevka2001

Copy link
Copy Markdown
Contributor Author

Fixed. cbm_watcher_free now sets stopped=1 as a safety net, documented the precondition in the header, and added a test for the unwatch -> pending_free drain path. 😃

@DeusData DeusData added bug Something isn't working ux/behavior Display bugs, docs, adoption UX priority/normal Standard review queue; useful PR with ordinary maintainer urgency. labels Jun 29, 2026
@DeusData

DeusData commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Thank you — this is a real bug with a thoughtful fix. We verified the zombie is worse than reported: the watcher's index_fn recreates the deleted .db, so deleted projects actually resurrect. Your deferred-free design is the right call given poll_once polls outside the lock (a naive immediate free would be a genuine cross-thread UAF). Two follow-ups we'll track separately: the UI DELETE /api/project path has the same zombie (no watcher access at all), and the realloc-failure fallback frees immediately (reintroducing the UAF in the OOM corner) — neither blocks this. Merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working priority/normal Standard review queue; useful PR with ordinary maintainer urgency. ux/behavior Display bugs, docs, adoption UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants